<?php

/**
 * PeopleManagement
 *
 * @copyright  Copyright (c) 2011 Celebrio Software
 * @package    PeopleManagement
 */

namespace ApplicationsModule\PeopleModule;

use Nette\Object;
use Nette\String;
use Nette\Environment;

use \dibi;
use \DibiFluent;
use \DibiDataSource;
use \DibiRow;

use Celebrio\Core\ServiceProvider;
use \Celebrio\Core\NoSuchServiceException;
use Celebrio\Modules;
use \Celebrio\AccessControl\BaseUser;

use \InvalidArgumentException;

/**
 * People Management
 *
 * @author martin, pavel
 */
class PeopleManagement extends Object {

    const PHOTOS_DIR = "/app/people/photos/";
    const AVATAR = "/app/people/avatar.png";

    /**
     * @beware has string reference (in people init)
     * @param Person $person
     * @return void
     */
    public function createPerson(BaseUser $person) {

        // if generic user is given, create person object
        if (!($person instanceof Person)) {
            $new_person = new Person($person->getFirstName(), $person->getLastName());
            $new_person->setUserId($person->getUserId());
            $person = $new_person;
        }

        $personArray = array(
            "first_name_person" => $person->getFirstName(),
            "last_name_person" => $person->getLastName(),
            "id_user" => Environment::getUser()->getIdentity()->id, // new person is connected to current user
        );

        dibi::begin();
        dibi::insert("people", $personArray)->execute();
        $person->setId(dibi::getInsertId());

        if ($person->getProfilePicture() !== null) {

            $last = String::webalize($person->getLastName());
            $first = String::webalize($person->getFirstName());
            $path = self::PHOTOS_DIR . $person->getId() . "-$last-$first.jpg";
            
            //todo osetrit kdyz to nebude jpg, evidentne zatim funguje i pro png
            if ($this->savePictureToVfs($path, $person->getProfilePicture())) {
                dibi::update("people", array("photo_person" => $path))
                        ->where(array("id_person" => $person->getId()))->execute();
            }
        }

        //todo get empty array from service provider instead of catching exception
        try {
            $contact_services = ServiceProvider::getAllServices(Modules::CONTACTS);

            foreach ($contact_services as $cs) {
                $contact_info = array("id_person" => $person->getId());
                $any_info = false;
                foreach ($cs::providedRows() as $info_key) {
                    $info_item = $person->getInfoField($info_key["db"]);
                    if (!empty($info_item)) {
                        $any_info = true;
                        $contact_info = array_merge($contact_info, array($info_key["db"] => $info_item));
                    }
                }
                if ($any_info) {
                    //todo update record for already existing contact or create new? exception for now...
                    dibi::insert("contacts_" . $cs::getServiceName(), $contact_info)->execute();
                }
            }
        } catch (NoSuchServiceException $e) {
            // no contact services found, do nothing
        }

        if ($person->getUserId() !== null) {
            $person->connectToUser();
        }

        dibi::commit();
    }

    public function getAllPeople() {
        return $this->peopleToArray($this->selectFromPeopleJoinAll()->toDataSource());
    }

    /**
     * @param $info
     * @return array of Person
     */
    public function searchExisting($info) {
        $info = array_filter($info); // filter empty fields

        $existing_people = $this->peopleToArray(
                $this->selectFromPeopleJoinAll()
                        ->where("%and", $info)
                        ->toDataSource()
            );
        return $existing_people;
    }

    public function getFriends($userId = null) {

        if ($userId === null) {
            $userId = Environment::getUser()->getIdentity()->id;
        }
        
        $friendsDs = $this->selectFromPeopleJoinAll()
                ->where(array("id_user" => $userId))
                ->toDataSource();
        return $this->peopleToArray($friendsDs);
        
    }

    public function getPerson($personId) {
        $personDb = $this->selectFromPeopleJoinAll(/* include protected => */ true)
                ->where(array("people.id_person" => $personId))->fetchAll();

        $multiple_records_for_person = array();
        foreach ($personDb as $personRow) {
            $multiple_records_for_person[] = $this->personRowToObject($personRow);
        }
        if (empty($multiple_records_for_person)) {
            // there is not even 1 line matching requested person
            return null;
        } else {
            // there is at least 1 line matching requested person
            return $this->reduceMultiple($multiple_records_for_person);
        }
    }

    /**
     * @todo move it somewhere else
     *
     * @throws \InvalidStateException
     * @param $personId
     * @param $mail
     * @return bool
     */
    public function addMail($personId, $mail) {
        $existing = dibi::select("*")->from("contacts_mail")
                ->where(array("id_person" => $personId, "mail_address" => $mail))
                ->count();

        if ($existing === 0) {
            dibi::insert("contacts_mail", array("mail_address" => $mail, "id_person" => $personId))->execute();
            return true; // added
        } elseif ($existing === 1) {
            return false; // already exists
        } else {
            throw new \InvalidStateException("Same e-mail address assigned $existing times to 1 person.");
        }
    }


    // ----- PRIVATE... --------------------- //


    /**
     * Saves picture to the VFS
     *
     * @param string $path Path in VFS the picture should be saved
     * @param string $pictureUrl URL to the actual picture file
     * @return bool TRUE when URL is valid and picture was saved, FALSE otherwise
     */
    private function savePictureToVfs($path, $pictureUrl) {
        $fs = ServiceProvider::getService(Modules::FS);
        $data = @file_get_contents($pictureUrl);
        if ($data === false) {
            return false;
        }
        $fs->save($path, $data);
        return true;
    }

    private function peopleToArray(DibiDataSource $dataSource) {
        $people_with_duplicates = array();
        foreach ($dataSource as $person) {
            $p = $this->personRowToObject($person);
            if (array_key_exists($p->getId(), $people_with_duplicates)) {
                array_push($people_with_duplicates[$p->getId()], $p);
            } else {
                $people_with_duplicates[$p->getId()] = array($p);
            }
        }
        $people = array();
        foreach ($people_with_duplicates as $person_with_duplicates) {
            $people[] = $this->reduceMultiple($person_with_duplicates);
        }
        return $people;
    }

    /**
     * Transforms Person DibiRow to the appropriate object
     *
     * @param \DibiRow $personDb
     * @return \ApplicationsModule\PeopleModule\Person new person object
     */
    private function personRowToObject(DibiRow $personDb) {
        $person = new Person();
        $person->setId($personDb->id_person);
        $person->setFirstName($personDb->first_name_person);
        $person->setLastName($personDb->last_name_person);
        if (empty($personDb->photo_person)) {
            $pictureInVfs = self::AVATAR;
        } elseif (String::startsWith($personDb->photo_person, self::PHOTOS_DIR)) {
            $pictureInVfs = $personDb->photo_person;
        } else {
            throw new \Exception("picture path does not belong to ppl app in vfs)");
        }
        $person->setProfilePicture(Environment::getVariable("baseUri")
                . "api/rest/load?out=image&what=" . $pictureInVfs);

        $known_columns = array("id_person", "first_name_person", "last_name_person", "photo_person");
        foreach ($personDb as $col_name => $col_value) {
            if (!in_array($col_name, $known_columns) && !String::startsWith($col_name, "id_")) {
                $person->addToAdditionalInfo($col_name, $col_value);
            }
        }
        return $person;
    }

    /**
     * Performs select over all contact_* tables join people. The join is outer
     * so every line which is present in the people table should be in the result.
     *
     * Variables x, y, z in the return statement are names of the contact module tables.
     * Each of them is supposed to be named "contacts_*" and references people table by
     * "id_person" column.
     *
     * @return DibiFluent SELECT * FROM people LEFT JOIN x LEFT JOIN y LEFT JOIN z ...
     *
     * @todo check the performance, this _might_ be bottleneck for the database
     */
    private function selectFromPeopleJoinAll($include_protected = false) {

        $selected_columns = PeopleItems::info_items(array(PeopleItems::INCLUDE_PROTECTED => $include_protected));

        array_push($selected_columns, "people.id_person");

        $query = dibi::select($selected_columns)->from("people");
        foreach (ServiceProvider::getAllServices(Modules::CONTACTS) as $contact_service) {
            $service_name = $contact_service::getServiceName();
            $query->leftJoin("contacts_" . $service_name)->as($service_name)
                    ->on("[people].[id_person] = [$service_name].[id_person]");
        }
        
        return $query;
    }

    /**
     * Takes multiple Person objects (array) belonging to the same person and "reduces" them to single
     * object containing all the information, for example multiple e-mail addresses. All the information
     * is usually stored in additional info field.
     *
     * @throws \InvalidArgumentException
     * @param array $multiple
     * @return \ApplicationsModule\PeopleModule\Person
     */
    private function reduceMultiple(array $multiple) {
        $check = $multiple[0]->getId();
        $reduced = clone($multiple[0]); // let's use first person record
        $reduced->clearAdditionalInfo();
        foreach ($multiple as $person) {
            if ($person->getId() != $check) throw new InvalidArgumentException("IDs don't fit, different people came as arguments...");
            foreach ($person->getAdditionalInfo() as $info_key => $info_value_array) {
                $reduced->addToAdditionalInfo($info_key, $info_value_array[0]);
            }
        }
        return $reduced;
    }

}